package org.windowkit.toolkit.print;

import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.text.MessageFormat;

import javax.swing.JComponent;
import javax.swing.RepaintManager;



/**
 * Abstract printer takes care of printing tasks that would normally be repeated
 * in each type of print class. Any extending class will normally only have to
 * implement some simple code in paintPage to paint the component. Painting of
 * the header and footer is taken care of by this class as well as basic
 * translation of the graphics object.
 *
 * DefaultPrinter should take care of printing most components unless custom
 * painting must be done.
 *
 * @author Eric Lundin
 *
 */
public abstract class AbstractPrinter implements Printable {
	/**
	 * Format for page number string to have all components print page numbers
	 * the same.
	 */
	protected static final String PAGE_NUMBER_FORMT_STRING = "PAGE {0,number} OF {1,number}";

	/**
	 * Padding for strings in the x direction.
	 */
	protected static final int X_PAD = 4;

	/**
	 * Padding for strings in the y direction.
	 */
	protected static final int Y_PAD = 4;

	public static final int X_AXIS = 0;

	public static final int Y_AXIS = 1;

	/** The component to print. */
	protected JComponent component;

	/** The header to display on all pages above the component. */
	protected JComponent header;

	/**
	 * The message to display at the bottom of the current page.
	 */
	protected String pageMessage;

	/** The component to print's size */
	protected Dimension compSize;

	/** The size of the component header. */
	protected Dimension headerSize;

	/** The page height */
	protected double pageHeight;

	/** The page width */
	protected double pageWidth;

	/** The component width. */
	protected double compWidth;

	/** The component height */
	protected double compHeight;

	/** The header width */
	protected double headerWidth;

	/** The header height */
	protected double headerHeight;

	/** Allows page numbers to be easily printed the same way everytime. */
	MessageFormat pageNumberFormat;

	/**
	 * Stores the title to display at the top of each page.
	 */
	protected String title;

	/**
	 * Used to round the amount of the component to put on one page so that no
	 * data is cutoff.
	 *
	 */
	protected double heightIncrement;

	/**
	 * Setup a printer
	 *
	 * @param component
	 *            The component to print.
	 * @param header
	 *            The header to display above the component. In order for this
	 *            to be effective the header must be as wide as the component.
	 *            This is mostly for tables which have columns whose total width
	 *            is equal to its header.
	 */
	public AbstractPrinter(final JComponent component, final JComponent componentHeader,
			final String title) {
		super();
		init(component, componentHeader);
		intializeAttributes();
		setHeaderSize(componentHeader);
		setComponent(component);
		setTitle(title);
	}

	public void setTitle(final String title) {
		this.title = title;
	}

	/**
	 * Initialize the attributes.
	 */
	protected void intializeAttributes() {
		pageNumberFormat = new MessageFormat(PAGE_NUMBER_FORMT_STRING);
	}

	/**
	 * Set the component to print as the given component. Store the size of the
	 * component.
	 *
	 * @param component
	 *            The component to print.
	 */
	protected void setComponent(final JComponent component) {
		this.component = component;
		compSize = component == null ? new Dimension(0, 0) : component
				.getSize();
		compWidth = compSize.width;
		compHeight = compSize.height;
	}

	/**
	 * Sets the header to print above each page of the component. Stores the
	 * size of the header. The header should be the same width as the component.
	 *
	 * @param header
	 *            The header to print at the top of each page.
	 */
	protected void setHeaderSize(final JComponent header) {
		this.header = header;
		headerSize = header == null ? new Dimension(0, 0) : header.getSize();
		headerWidth = headerSize.width;
		headerHeight = headerSize.height;
	}

	/**
	 * This method is called by the print service to render the printed image.
	 */
	public int print(final Graphics graphics, final PageFormat pageFormat, final int pageIndex)
			throws PrinterException {
		int response;
		Graphics2D g2 = (Graphics2D) graphics;

		pageMessage = createPageNumberString(pageIndex, getTotalNumberOfPages(pageFormat,g2));
		pageHeight = pageFormat.getImageableHeight();
		pageWidth = pageFormat.getImageableWidth();
		setPageMessage(createPageNumberString(pageIndex,
				getTotalNumberOfPages(pageFormat,g2)));
		if (pageIndex < getTotalNumberOfPages(pageFormat, g2)) {
			disableDoubleBuffering(header);
			disableDoubleBuffering(component);
			setRenderingHints(g2);
			paintPage(pageFormat, pageIndex, g2);
			enableDoubleBuffering(header);
			enableDoubleBuffering(component);
			response = PAGE_EXISTS;
		} else {
			response = NO_SUCH_PAGE;
		}
		return response;
	}

	/**
	 * Get the total number of pages this job will take.
	 *
	 * @return The total number of pages needed to complete this job.
	 */
	protected int getTotalNumberOfPages(final PageFormat format, final Graphics2D g) {
		return getPagesHigh(format,g) * getPagesWide(format);
	}

	/**
	 * Paints a page given the page index and graphics object. This method
	 * assumes that the page represented by pageIndex has already been validated
	 * as a valid page.
	 *
	 * @param pageFormat
	 *            The page format used for this printing job.
	 * @param pageIndex
	 *            The index of the current page. The indicies are ordered from 0 ->
	 *            (n-1) where n is the total number of pages. The page indicies
	 *            are assigned to the grid going from left to right then
	 *            downwards.
	 * @param g2
	 *            The graphics object used to paint the component.
	 * @param totalNumPages
	 *            The total number of pages this print job needs to print the
	 *            entire component.
	 */
	protected abstract void paintPage(PageFormat pageFormat, int pageIndex,
			Graphics2D g2);

	/**
	 * Gets the size of the given string.
	 *
	 * @param msg
	 *            The string to find the dimensions of.
	 * @param g2
	 *            The graphics object which will render the string.
	 * @return The width and height of the given string.
	 */
	protected Dimension getStringDimensions(final String msg, final Graphics g) {
		FontMetrics fm = g.getFontMetrics();
		Dimension retVal = new Dimension();
		retVal.setSize(fm.stringWidth(msg), fm.getLineMetrics(msg, g)
				.getHeight());
		return retVal;
	}

	/**
	 * Get the dimensions of the page message.
	 *
	 * @param g
	 *            The graphics object which will render the message.
	 * @return The dimensions of the string.
	 */
	public Dimension getPageMessageDimensions(final Graphics g) {
		return getStringDimensions(getPageMessage(), g);
	}

	public Dimension getTitleDimensions(final Graphics g) {
		return getStringDimensions(getTitle(), g);
	}

	public String getTitle() {
		return title;
	}

	/**
	 * Parses the page number and total number of pages into a common format of
	 * string.
	 *
	 */
	protected String createPageNumberString(final int pageIndex, final int totalNumPages) {
		return MessageFormat.format(PAGE_NUMBER_FORMT_STRING, pageIndex + 1,
				totalNumPages);
	}

	/**
	 * Sets rendering hints for printing.
	 *
	 * @param g2
	 *            The Graphics object to set the rendering hints on.
	 */
	protected void setRenderingHints(final Graphics2D g2) {
		RenderingHints renderHints = new RenderingHints(
				RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		renderHints.put(RenderingHints.KEY_RENDERING,
				RenderingHints.VALUE_RENDER_QUALITY);
		g2.setRenderingHints(renderHints);
	}

	/**
	 * Calculates and returns the number of pages wide this component will be on
	 * paper.
	 *
	 * @return The number of pages wide this component will be.
	 */
	protected int getPagesWide(final PageFormat format) {
		return getPagesWide(format.getImageableWidth());
	}

	/**
	 * Get the height increment. The height increment is used to determine how
	 * to round off the amount of the component printed on each page in the y
	 * direction to avoid cutting off data.
	 *
	 * @return The height increment.
	 */
	protected double getHeightIncrement() {
		return heightIncrement;
	}

	/**
	 * Given the height of one page determine the number of pages wide this job
	 * is.
	 *
	 * @param pageHeight
	 *            The height of the page.
	 * @return The number of pages wide this job will be.
	 */
	protected int getPagesWide(final double pageWidth) {
		int pagesWide = (int) Math.ceil(compWidth / pageWidth);
		return pagesWide;
	}

	/**
	 * Given the height of one page determine the number of pages high this job
	 * is.
	 *
	 * @param pageHeight
	 *            The height of the page.
	 * @return The number of pages high this job will be.
	 */
	protected int getPagesHigh(final PageFormat format, final Graphics2D g) {
		int pagesHigh = 0;
		pagesHigh = (int) Math.ceil(compHeight / getYCellHeight(format, g));/*(pageHeight - headerHeight));*/
		return pagesHigh;
	}

//	/**
//	 * Calculates and returns the number of pages high this component will be on
//	 * paper.
//	 *
//	 * @return The number of pages high this component will be.
//	 */
//	protected int getPagesHigh(PageFormat format, Graphics2D graphics) {
//		return getPagesHigh(pageHeight,graphics);
//	}

	/**
	 * Disable double buffering on a given component to print.
	 *
	 * @param component
	 *            The component to disable double buffering on.
	 */
	protected void disableDoubleBuffering(final JComponent component) {
		if (component != null) {
			RepaintManager.currentManager(component).setDoubleBufferingEnabled(
					false);
		}
	}

	/**
	 * Enable double buffering on a given component.
	 *
	 * @param component
	 *            The component to turn double buffering off on.
	 */
	protected void enableDoubleBuffering(final JComponent component) {
		if (component != null) {
			RepaintManager.currentManager(component).setDoubleBufferingEnabled(
					true);
		}
	}

	/**
	 * Sets the unit which the current clipping area of the printed component
	 * will be rounded to, this allows cutting off data to be avoided. For
	 * example, when printing a table cutting a row in half is undesirable
	 * therefore passing in the rowheight will allow the exending class to
	 * adjust the amount of data printed to each page so that no rows are cut
	 * off.
	 *
	 * @param increment
	 *            The increment to use for determining height.
	 */
	public void setHeightIncrement(final double increment) {
		heightIncrement = increment;
	}

	/**
	 * Initialize the fields.
	 *
	 * @param component
	 *            The component to print.
	 * @param header
	 *            The header to display above the component. In order for this
	 *            to be effective the header must be as wide as the component.
	 *            This is mostly for tables which have columns whose total width
	 *            is equal to its header.
	 */
	private void init(final JComponent component, final JComponent componentHeader) {
		if (component == null) {
			throw new NullPointerException("Component to print cannot be null");
		}
		this.component = component;
		this.header = componentHeader;
	}

	/**
	 * Get the height of a page in pixels.
	 *
	 * @return The height of a page in pixels.
	 */
	public int getPageHeight() {
		return (int) pageHeight;
	}

	/**
	 * Get the width of a page in pixels.
	 *
	 * @return The width of a page in pixels.
	 */
	public int getPageWidth() {
		return (int) pageWidth;
	}

	/**
	 * Set the page message.
	 *
	 * @param pageMessage
	 *            The page message this class wil display on the bottom of the
	 *            page.
	 */
	public void setPageMessage(final String pageMessage) {
		this.pageMessage = pageMessage;
	}

	/**
	 * Get the page message for the current page.
	 *
	 * @return The page message to display.
	 */
	public String getPageMessage() {
		return (pageMessage != null?pageMessage:createPageNumberString(0,0));
	}

//	/**
//	 * Gets the number of pages needed to print this component in the given page
//	 * format.
//	 */
//	public int getTotalNumberOfPages(PageFormat format,Graphics2D g) {
//		double imageableWidth = format.getImageableWidth();
//		double imageableHeight = format.getImageableHeight();
//		return getPagesHigh(format,g) * getPagesWide(format);
//	}

	/**
	 * Gets the height of one y cell of the component.
	 *
	 * @param pageFormat
	 *            The page format currently being used.
	 * @return The amount of component to print on one page in the Y direction.
	 */
	protected double getYCellHeight(final PageFormat pageFormat, final Graphics g) {
		int pageMsgHeight = getPageMessageDimensions(g).height+getStringPadding(Y_AXIS);
		double imageableHeight = pageFormat.getImageableHeight();
		int titleHeight = getTitleDimensions(g).height+getStringPadding(Y_AXIS);
		double cellHeight = roundToIncrement(imageableHeight - headerHeight
				- pageMsgHeight - titleHeight);
		return cellHeight;
	}

	/**
	 * Since we may want to find the height of the page number string before we
	 * have a graphics operation create a string using all numbers in case the
	 * font has numbers with varying heights.
	 *
	 * @param g
	 *            The graphics object to use.
	 * @return The
	 */
	public Dimension getDefaultPageMessageDimension(final Graphics g) {
		return getStringDimensions(getPageMessage(), g);
	}

	/**
	 * Rounds a given value to the next smallest multiple of the height
	 * increment. For example if we have a table with a row height of 4 and a
	 * page size of 10 we do not want a row to be cut off. Therefore we only
	 * print 8 units worth of the component per page.
	 *
	 * @param amount
	 *            The amount to round.
	 * @return The rounded value.
	 */
	protected double roundToIncrement(final double amount) {
		double newAmount = amount - (amount % (getHeightIncrement()));
		return newAmount;
	}

	/**
	 * Get the string padding amount in the axis specified.
	 *
	 * @param axis
	 *            The axis to get the padding for. Either AbstractPrinter.X_AXIS
	 *            or AbstractPrinter.Y_AXIS.
	 * @return The amount to pad the string in the given direciton.
	 */
	protected int getStringPadding(final int axis) {
		int padding = 0;
		switch (axis) {
		case X_AXIS:
			padding = X_PAD;
			break;
		case Y_AXIS:
			padding = Y_PAD;
			break;
		}
		return padding;
	}
}
